home *** CD-ROM | disk | FTP | other *** search
/ Skunkware 98 / Skunkware 98.iso / osr5 / sco / scripts / mail / mailalert < prev    next >
Encoding:
AWK Script  |  1997-08-26  |  35.3 KB  |  976 lines

  1. #!/usr/local/bin/gawk -f
  2. # @(#) mailalert.gawk 1.3 96/11/14
  3. # 92/03/20 John DuBois (john@armory.com)
  4. # 92/11/07 Added Uncontrol filtering
  5. # 93/02/01 Separated utty and id into functions
  6. # 94/02/27 Arrange for process to be killed eventually if one of the tty
  7. #          writes hangs.
  8. # 94/03/14 Close kill command.
  9. # 94/03/24 Added nobeep option
  10. # 94/07/14 Use gawk, so user name can be given on cmd line & input still read
  11. #          from stdin.  Added all options.
  12. # 95/06/12 Include date in alerts.
  13. # 95/06/17 Added [enwaxX] options.
  14. # 96/05/22 Added tV options.
  15. # 96/11/14 Use cat to write to tty.
  16.  
  17. # Options not listed by -h:
  18. # -x: Turn on debugging.
  19. # -e<errorfile>: Send error output to errorfile instead of stderr.
  20.  
  21. BEGIN {
  22.     Name = "mailalert"
  23.     Usage = "Usage: " Name " [-hbnwatV] [username]"
  24.     ErrorFile = "/dev/stderr"
  25.     ARGC = Opts(Name,Usage,"hbnwatVxe:",0)
  26.     if ("h" in Options) {
  27.     printf \
  28. "%s: Notify users of new mail.\n"\
  29. "%s\n"\
  30. "%s is used to notify a user if the user receives mail while logged in.\n"\
  31. "Note: in the following, 'writable' refers to a tty that has been set\n"\
  32. "writable by the 'mesg' command, and being logged on a tty refers to\n"\
  33. "having a utmp entry for that tty.\n"\
  34. "%s is set up by putting this line in the user's .maildelivery file:\n"\
  35. "* - pipe R /usr/local/bin/mailalert\n"\
  36. "(note that mailalert may be somewhere other than /usr/local/bin).  \n"\
  37. "With this line in place, whenever mail is delivered, %s will be run.\n"\
  38. "It will check to see if the user is logged in and writable on any tty; if\n"\
  39. "so, a message informing the user of new mail is written to each such tty.\n"\
  40. "If a user name is given, messages are written to any of that user's ttys\n"\
  41. "whose tty permissions allow writing by the user receiving mail.\n"\
  42. "Options:\n"\
  43. "-h: Print this help.\n"\
  44. "-b: Do not send beeps with the message written to the ttys.\n"\
  45. "-n: Send a message to every tty the user is logged in on, whether writable\n"\
  46. "    or not.\n"\
  47. "-w: If the user is writable on any ttys, send messages to those ttys; if\n"\
  48. "    not, send messages to non-writable ttys.\n"\
  49. "-a: Send a message only to the most recently modified tty (hopefully, the\n"\
  50. "    tty the user is currently active on).  If -w is used with this option,\n"\
  51. "    %s will search first for the most recently modifed writable tty,\n"\
  52. "    then the most recently modified non-writable tty.\n"\
  53. "-t: Test mailalert by generating pseudo-data instead of reading a mail\n"\
  54. "    message from the standard input.\n"\
  55. "-V: Verbose mode.  A description of what %s is doing as it searches\n"\
  56. "    for ttys to write to is printed on the standard output.\n",
  57. Name,Usage,Name,Name,Name,Name,Name
  58.         exit 0
  59.     }
  60.     if ("e" in Options)
  61.     ErrorFile = Options["e"]
  62.     if ((Err = ExclusiveOptions("n,wa",Options)) != "") {
  63.     printf "mailalert: Error: " Err "\n" > ErrorFile
  64.     exit(1)
  65.     }
  66.     nobeep = "b" in Options
  67.     allTTYs = "n" in Options || (writableFirst = "w" in Options)
  68.     recentFirst = "a" in Options
  69.     Debug = "x" in Options
  70.     Verbose = "V" in Options || Debug
  71.     PsuedoData = "t" in Options
  72.     if (ARGC > 1) {
  73.     if (ARGC > 2) {
  74.         printf "mailalert: Error: Too many arguments given.  Exiting.\n"\
  75.         > ErrorFile
  76.         exit(1)
  77.     }
  78.     User = ARGV[1]
  79.     }
  80.     else
  81.     User = id()
  82.     if (!(NumTTYs = utty(User,TTYs,Age))) {
  83.     if (Verbose)
  84.         printf "%s is not logged in on any ttys.  Exiting.\n",User
  85.     exit(0)
  86.     }
  87.     else if (Verbose)
  88.     printf "%s is logged in on %s tty(s).\n",User,NumTTYs
  89.     if (PsuedoData) {
  90.     MStat["Lines"] = 99
  91.     MStat["From"] = "From: Someone@Somewhere"
  92.     MStat["Subject"] = "Subject: This is a test, this is only a test..."
  93.     }
  94.     else
  95.     GetMailStat(MStat)
  96.     MakeUncontrolTable()
  97.     # If any ttys are writable, arrange to discard non-writable ttys
  98.     if (writableFirst)
  99.     for (tty in TTYs)
  100.         if (TTYs[tty]) {
  101.         if (Verbose)
  102.             printf "Found a writable tty (%s).\n",tty
  103.         allTTYs = 0
  104.         break
  105.         }
  106.     if (!allTTYs)    # discard non-writable ttys
  107.     for (tty in TTYs)
  108.         if (!TTYs[tty]) {
  109.         if (Verbose)
  110.             printf "Discarding non-writable tty: %s\n",tty
  111.         NumTTYs--
  112.         delete TTYs[tty]
  113.         delete Age[tty]
  114.         }
  115.     if (!NumTTYs)
  116.     exit(0)
  117.     # TTY open may hang in certain circumstances due to modem control, etc.
  118.     # Give writes 5 seconds per tty.
  119.     if (recentFirst)
  120.     KillMe(NumTTYs)
  121.     else
  122.     KillMe(5*NumTTYs)
  123.     if (Verbose)
  124.     printf "%d tty(s) left.\n",NumTTYs
  125.     if (recentFirst)
  126.     arrMin(Age,TTYs)
  127.     for (tty in TTYs) {
  128.     if (Verbose)
  129.         printf "biffing /dev/%s\n",tty
  130.     biff("/dev/" tty,MStat,!nobeep)
  131.     }
  132. }
  133.  
  134. # Arrange for this process to be killed in Sec seconds.
  135. # Stores PID of backgrounded shell in global KillPID, and also returns it,
  136. # so that this can be aborted.
  137. function KillMe(Sec,  Cmd) {
  138.     # Use ksh because it has $PPID.  
  139.     # Exec it so $PPID will be correct.
  140.     # Go into background as a subshell within ksh because if the exec'ed
  141.     # ksh is run in the background it will have a different $PPID since it
  142.     # will have to be forked to go into the background,
  143.     # while () preserves $PPID even when put in the background.  
  144.     # Echo $! so we will have a PID to kill to abort this.  
  145.     # Use newline to separate because ; doesn't work with &.
  146.     Cmd = sprintf("exec /bin/ksh -c '(sleep %d; kill -9 $PPID 2>/dev/null)&\n"\
  147.     "echo $!'", Sec)
  148.     Cmd | getline KillPID
  149.     close (Cmd)
  150.     return KillPID
  151. }
  152.  
  153. #function AbortKill() {
  154. #    return system("kill -9 " KillPID)
  155. #}
  156.  
  157. # utty: find ttys a user is logged in on.
  158. # For each tty User is logged in on, an element is created in TTYs[].
  159. # The index is the name of the tty, with a leading "/dev/".
  160. # The value is set to 1 if the user is writable on that tty, 0 if not.
  161. # The time since there was activity on the tty (in minutes) is returned in
  162. # Age, indexed the same as TTYs.
  163. # The number of ttys the user is logged in on is returned.
  164. function utty(User,TTYs,Age,  Cmd,Count) {
  165. # who -uT output looks like this:
  166. # max      - ttyp30       Jun 15 09:45  0:09   3787
  167.     Cmd = "exec who -uT"
  168.     Count = 0
  169.     while ((Cmd | getline) == 1)
  170.     if (NF == 8 && $1 == User) {
  171.         if ($2 == "+")
  172.         TTYs[$3] = 1
  173.         else
  174.         TTYs[$3] = 0
  175.         Count++
  176.         Age[$3] = hm2m($7)
  177.     }
  178.     close(Cmd)
  179.     return Count
  180. }
  181.  
  182. # Given a time string of the form m or mm or h:mm, return the time in minutes.
  183. function hm2m(HM) {
  184.     if (split(HM,elem,":") > 1)
  185.     return elem[1]*60 + elem[2]
  186.     else
  187.     return elem[1]
  188. }
  189.  
  190. # id returns the login name of the user who owns the current process
  191. function id(  Cmd,line,elem) {
  192.     Cmd = "exec /usr/bin/id"
  193.     Cmd | getline line
  194.     split(line,elem,"[()]")
  195.     close(Cmd)
  196.     return elem[2]
  197. }
  198.  
  199. # This function returns the date in the system timezone.
  200. # We must do this because pipes in .maildelivery are run without TZ set,
  201. # and setting TZ doesn't affect gawk's strftime function.
  202. function LocalDate(  Cmd,line) {
  203.     Cmd = ". /etc/TIMEZONE; exec /bin/date '+%a %h %d %H:%M %Z'"
  204.     Cmd | getline line
  205.     close(Cmd)
  206.     return line
  207. }
  208.  
  209. function biff(tty,MStat,beep,  message) {
  210.     Message = sprintf("%s\r\nNew mail received %s (%d lines):\r\n",
  211.     (beep ? "\007" : ""), LocalDate(), MStat["Lines"])
  212.     if ("From" in MStat)
  213.     Message = Message Uncontrol(MStat["From"]) "\r\n"
  214.     if ("Subject" in MStat)
  215.     Message = Message Uncontrol(MStat["Subject"]) "\r\n"
  216.     # Use cat to write to tty because if awk tries to open the tty and fails,
  217.     # it will abort, preventing it from going on to the next tty.
  218.     Cmd = "cat > " tty
  219.     printf "%s",Message | Cmd
  220.     close(Cmd)
  221. }
  222.  
  223. function GetMailStat(MStat,  InHeader,LineCt) {
  224.     InHeader = 1
  225.     LineCt = 0
  226.     while ((getline < "/dev/stdin") == 1) {
  227.     LineCt++
  228.     if (InHeader) {
  229.         if (Debug)
  230.         printf "Header line %d: %s\n",LineCt,$0
  231.         if (!NF) {
  232.         InHeader = 0
  233.         continue
  234.         }
  235.         if ($1 == "From:")
  236.         MStat["From"] = $0
  237.         else if ($1 == "Subject:")
  238.         MStat["Subject"] = $0
  239.     }
  240.     }
  241.     MStat["Lines"] = LineCt
  242. }
  243.  
  244. # Uncontrol(S): Convert control characters in S to symbolic form.
  245. # Characters in S with values < 32 are converted to the form ^X.
  246. # The resulting string is returned.
  247. # Global variables: the array UncTable must be initialized to a
  248. # lookup table translating characters 0..31 to symbolic values
  249. # before using this function.
  250. function Uncontrol(S,  i,len,Output) {
  251.     len = length(S)
  252.     Output = ""
  253.     for (i = 1; i <= len; i++)
  254.     Output = Output UncTable[substr(S,i,1)]
  255.     return Output
  256. }
  257.  
  258. # MakeUncontrolTable: Make a table for use by Uncontrol().
  259. # Global variables: 
  260. # UncTable[] is made into a character -> symbolic character lookup table.
  261. function MakeUncontrolTable(  i) {
  262.     for (i = 0; i < 32; i++)
  263.     UncTable[sprintf("%c",i)] = "^" sprintf("%c",i + 64)
  264.     for (i = 32; i < 127; i++)
  265.     UncTable[sprintf("%c",i)] = sprintf("%c",i)
  266.     UncTable[sprintf("%c",127)] = "^?"
  267.     for (i = 128; i < 256; i++)
  268.     UncTable[sprintf("%c",i)] = "\\" sprintf("%03o",i)
  269. }
  270.  
  271. ### Start of ProcArgs library
  272. # @(#) ProcArgs 1.11 96/12/08
  273. # 92/02/29 john h. dubois iii (john@armory.com)
  274. # 93/07/18 Added "#" arg type
  275. # 93/09/26 Do not count -h against MinArgs
  276. # 94/01/01 Stop scanning at first non-option arg.  Added ">" option type.
  277. #          Removed meaning of "+" or "-" by itself.
  278. # 94/03/08 Added & option and *()< option types.
  279. # 94/04/02 Added NoRCopt to Opts()
  280. # 94/06/11 Mark numeric variables as such.
  281. # 94/07/08 Opts(): Do not require any args if h option is given.
  282. # 95/01/22 Record options given more than once.  Record option num in argv.
  283. # 95/06/08 Added ExclusiveOptions().
  284. # 96/01/20 Let rcfiles be a colon-separated list of filenames.
  285. #          Expand $VARNAME at the start of its filenames.
  286. #          Let varname=0 and -option- turn off an option.
  287. # 96/05/05 Changed meaning of 7th arg to Opts; now can specify exactly how many
  288. #          of the vars should be searched for in the environment.
  289. #          Check for duplicate rcfiles.
  290. # 96/05/13 Return more specific error values.  Note: ProcArgs() and InitOpts()
  291. #          now return various negatives values on error, not just -1, and
  292. #          Opts() may set Err to various positive values, not just 1.
  293. #          Added AllowUnrecOpt.
  294. # 96/05/23 Check type given for & option
  295. # 96/06/15 Re-port to awk
  296. # 96/10/01 Moved file-reading code into ReadConfFile(), so that it can be
  297. #          used by other functions.
  298. # 96/10/15 Added OptChars
  299. # 96/11/01 Added exOpts arg to Opts()
  300. # 96/11/16 Added ; type
  301. # 96/12/08 Added Opt2Set() & Opt2Sets()
  302. # 96/12/27 Added CmdLineOpt()
  303.  
  304. # optlist is a string which contains all of the possible command line options.
  305. # A character followed by certain characters indicates that the option takes
  306. # an argument, with type as follows:
  307. # :    String argument
  308. # ;    Non-empty string argument
  309. # *    Floating point argument
  310. # (    Non-negative floating point argument
  311. # )    Positive floating point argument
  312. # #    Integer argument
  313. # <    Non-negative integer argument
  314. # >    Positive integer argument
  315. # The only difference the type of argument makes is in the runtime argument
  316. # error checking that is done.
  317.  
  318. # The & option is a special case used to get numeric options without the
  319. # user having to give an option character.  It is shorthand for [-+.0-9].
  320. # If & is included in optlist and an option string that begins with one of
  321. # these characters is seen, the value given to "&" will include the first
  322. # char of the option.  & must be followed by a type character other than ":"
  323. # or ";".
  324. # Note that if e.g. &> is given, an option of -.5 will produce an error.
  325.  
  326. # Strings in argv[] which begin with "-" or "+" are taken to be
  327. # strings of options, except that a string which consists solely of "-"
  328. # or "+" is taken to be a non-option string; like other non-option strings,
  329. # it stops the scanning of argv and is left in argv[].
  330. # An argument of "--" or "++" also stops the scanning of argv[] but is removed.
  331. # If an option takes an argument, the argument may either immediately
  332. # follow it or be given separately.
  333. # "-" and "+" options are treated the same.  "+" is allowed because most awks
  334. # take any -options to be arguments to themselves.  gawk 2.15 was enhanced to
  335. # stop scanning when it encounters an unrecognized option, though until 2.15.5
  336. # this feature had a flaw that caused problems in some cases.  See the OptChars
  337. # parameter to explicitly set the option-specifier characters.
  338.  
  339. # If an option that does not take an argument is given,
  340. # an index with its name is created in Options and its value is set to the
  341. # number of times it occurs in argv[].
  342.  
  343. # If an option that does take an argument is given, an index with its name is
  344. # created in Options and its value is set to the value of the argument given
  345. # for it, and Options[option-name,"count"] is (initially) set to the 1.
  346. # If an option that takes an argument is given more than once,
  347. # Options[option-name,"count"] is incremented, and the value is assigned to
  348. # the index (option-name,instance) where instance is 2 for the second occurance
  349. # of the option, etc.
  350. # In other words, the first time an option with a value is encountered, the
  351. # value is assigned to an index consisting only of its name; for any further
  352. # occurances of the option, the value index has an extra (count) dimension.
  353.  
  354. # The sequence number for each option found in argv[] is stored in
  355. # Options[option-name,"num",instance], where instance is 1 for the first
  356. # occurance of the option, etc.  The sequence number starts at 1 and is
  357. # incremented for each option, both those that have a value and those that
  358. # do not.  Options set from a config file have a value of 0 assigned to this.
  359.  
  360. # Options and their arguments are deleted from argv.
  361. # Note that this means that there may be gaps left in the indices of argv[].
  362. # If compress is nonzero, argv[] is packed by moving its elements so that
  363. # they have contiguous integer indices starting with 0.
  364. # Option processing will stop with the first unrecognized option, just as
  365. # though -- was given except that unlike -- the unrecognized option will not be
  366. # removed from ARGV[].  Normally, an error value is returned in this case.
  367. # If AllowUnrecOpt is true, it is not an error for an unrecognized option to
  368. # be found, so the number of remaining arguments is returned instead.
  369. # If OptChars is not a null string, it is the set of characters that indicate
  370. # that an argument is an option string if the string begins with one of the
  371. # characters.  A string consisting solely of two of the same option-indicator
  372. # characters stops the scanning of argv[].  The default is "-+".
  373. # argv[0] is not examined.
  374. # The number of arguments left in argc is returned.
  375. # If an error occurs, the global string OptErr is set to an error message
  376. # and a negative value is returned.
  377. # Current error values:
  378. # -1: option that required an argument did not get it.
  379. # -2: argument of incorrect type supplied for an option.
  380. # -3: unrecognized (invalid) option.
  381. function ProcArgs(argc,argv,OptList,Options,compress,AllowUnrecOpt,OptChars,
  382. ArgNum,ArgsLeft,Arg,ArgLen,ArgInd,Option,Pos,NumOpt,Value,HadValue,specGiven,
  383. NeedNextOpt,GotValue,OptionNum,Escape,dest,src,count,c,OptTerm,OptCharSet)
  384. {
  385. # ArgNum is the index of the argument being processed.
  386. # ArgsLeft is the number of arguments left in argv.
  387. # Arg is the argument being processed.
  388. # ArgLen is the length of the argument being processed.
  389. # ArgInd is the position of the character in Arg being processed.
  390. # Option is the character in Arg being processed.
  391. # Pos is the position in OptList of the option being processed.
  392. # NumOpt is true if a numeric option may be given.
  393.     ArgsLeft = argc
  394.     NumOpt = index(OptList,"&")
  395.     OptionNum = 0
  396.     if (OptChars == "")
  397.     OptChars = "-+"
  398.     while (OptChars != "") {
  399.     c = substr(OptChars,1,1)
  400.     OptChars = substr(OptChars,2)
  401.     OptCharSet[c]
  402.     OptTerm[c c]
  403.     }
  404.     for (ArgNum = 1; ArgNum < argc; ArgNum++) {
  405.     Arg = argv[ArgNum]
  406.     if (length(Arg) < 2 || !((specGiven = substr(Arg,1,1)) in OptCharSet))
  407.         break    # Not an option; quit
  408.     if (Arg in OptTerm) {
  409.         delete argv[ArgNum]
  410.         ArgsLeft--
  411.         break
  412.     }
  413.     ArgLen = length(Arg)
  414.     for (ArgInd = 2; ArgInd <= ArgLen; ArgInd++) {
  415.         Option = substr(Arg,ArgInd,1)
  416.         if (NumOpt && Option ~ /[-+.0-9]/) {
  417.         # If this option is a numeric option, make its flag be & and
  418.         # its option string flag position be the position of & in
  419.         # the option string.
  420.         Option = "&"
  421.         Pos = NumOpt
  422.         # Prefix Arg with a char so that ArgInd will point to the
  423.         # first char of the numeric option.
  424.         Arg = "&" Arg
  425.         ArgLen++
  426.         }
  427.         # Find position of flag in option string, to get its type (if any).
  428.         # Disallow & as literal flag.
  429.         else if (!(Pos = index(OptList,Option)) || Option == "&") {
  430.         if (AllowUnrecOpt) {
  431.             Escape = 1
  432.             break
  433.         }
  434.         else {
  435.             OptErr = "Invalid option: " specGiven Option
  436.             return -3
  437.         }
  438.         }
  439.  
  440.         # Find what the value of the option will be if it takes one.
  441.         # NeedNextOpt is true if the option specifier is the last char of
  442.         # this arg, which means that if the option requires a value it is
  443.         # the next arg.
  444.         if (NeedNextOpt = (ArgInd >= ArgLen)) { # Value is the next arg
  445.         if (GotValue = ArgNum + 1 < argc)
  446.             Value = argv[ArgNum+1]
  447.         }
  448.         else {    # Value is included with option
  449.         Value = substr(Arg,ArgInd + 1)
  450.         GotValue = 1
  451.         }
  452.  
  453.         if (HadValue = AssignVal(Option,Value,Options,
  454.         substr(OptList,Pos + 1,1),GotValue,"",++OptionNum,!NeedNextOpt,
  455.         specGiven)) {
  456.         if (HadValue < 0)    # error occured
  457.             return HadValue
  458.         if (HadValue == 2)
  459.             ArgInd++    # Account for the single-char value we used.
  460.         else {
  461.             if (NeedNextOpt) {    # option took next arg as value
  462.             delete argv[++ArgNum]
  463.             ArgsLeft--
  464.             }
  465.             break    # This option has been used up
  466.         }
  467.         }
  468.     }
  469.     if (Escape)
  470.         break
  471.     # Do not delete arg until after processing of it, so that if it is not
  472.     # recognized it can be left in ARGV[].
  473.     delete argv[ArgNum]
  474.     ArgsLeft--
  475.     }
  476.     if (compress != 0) {
  477.     dest = 1
  478.     src = argc - ArgsLeft + 1
  479.     for (count = ArgsLeft - 1; count; count--) {
  480.         ARGV[dest] = ARGV[src]
  481.         dest++
  482.         src++
  483.     }
  484.     }
  485.     return ArgsLeft
  486. }
  487.  
  488. # Assignment to values in Options[] occurs only in this function.
  489. # Option: Option specifier character.
  490. # Value: Value to be assigned to option, if it takes a value.
  491. # Options[]: Options array to return values in.
  492. # ArgType: Argument type specifier character.
  493. # GotValue: Whether any value is available to be assigned to this option.
  494. # Name: Name of option being processed.
  495. # OptionNum: Number of this option (starting with 1) if set in argv[],
  496. #     or 0 if it was given in a config file or in the environment.
  497. # SingleOpt: true if the value (if any) that is available for this option was
  498. #     given as part of the same command line arg as the option.  Used only for
  499. #     options from the command line.
  500. # specGiven is the option specifier character use, if any (e.g. - or +),
  501. # for use in error messages.
  502. # Global variables: OptErr
  503. # Return value: negative value on error, 0 if option did not require an
  504. # argument, 1 if it did & used the whole arg, 2 if it required just one char of
  505. # the arg.
  506. # Current error values:
  507. # -1: Option that required an argument did not get it.
  508. # -2: Value of incorrect type supplied for option.
  509. # -3: Bad type given for option &
  510. function AssignVal(Option,Value,Options,ArgType,GotValue,Name,OptionNum,
  511. SingleOpt,specGiven,  UsedValue,Err,NumTypes) {
  512.     # If option takes a value...    [
  513.     NumTypes = "*()#<>]"
  514.     if (Option == "&" && ArgType !~ "[" NumTypes) {    # ]
  515.     OptErr = "Bad type given for & option"
  516.     return -3
  517.     }
  518.  
  519.     if (UsedValue = (ArgType ~ "[:;" NumTypes)) {    # ]
  520.     if (!GotValue) {
  521.         if (Name != "")
  522.         OptErr = "Variable requires a value -- " Name
  523.         else
  524.         OptErr = "option requires an argument -- " Option
  525.         return -1
  526.     }
  527.     if ((Err = CheckType(ArgType,Value,Option,Name,specGiven)) != "") {
  528.         OptErr = Err
  529.         return -2
  530.     }
  531.     # Mark this as a numeric variable; will be propogated to Options[] val.
  532.     if (ArgType != ":" && ArgType != ";")
  533.         Value += 0
  534.     if ((Instance = ++Options[Option,"count"]) > 1)
  535.         Options[Option,Instance] = Value
  536.     else
  537.         Options[Option] = Value
  538.     }
  539.     # If this is an environ or rcfile assignment & it was given a value...
  540.     else if (!OptionNum && Value != "") {
  541.     UsedValue = 1
  542.     # If the value is "0" or "-" and this is the first instance of it,
  543.     # do not set Options[Option]; this allows an assignment in an rcfile to
  544.     # turn off an option (for the simple "Option in Options" test) in such
  545.     # a way that it cannot be turned on in a later file.
  546.     if (!(Option in Options) && (Value == "0" || Value == "-"))
  547.         Instance = 1
  548.     else
  549.         Instance = ++Options[Option]
  550.     # Save the value even though this is a flag
  551.     Options[Option,Instance] = Value
  552.     }
  553.     # If this is a command line flag and has a - following it in the same arg,
  554.     # it is being turned off.
  555.     else if (OptionNum && SingleOpt && substr(Value,1,1) == "-") {
  556.     UsedValue = 2
  557.     if (Option in Options)
  558.         Instance = ++Options[Option]
  559.     else
  560.         Instance = 1
  561.     Options[Option,Instance]
  562.     }
  563.     # If this is a flag assignment without a value, increment the count for the
  564.     # flag unless it was turned off.  The indicator for a flag being turned off
  565.     # is that the flag index has not been set in Options[] but it has an
  566.     # instance count.
  567.     else if (Option in Options || !((Option,1) in Options))
  568.     # Increment number of times this flag seen; will inc null value to 1
  569.     Instance = ++Options[Option]
  570.     Options[Option,"num",Instance] = OptionNum
  571.     return UsedValue
  572. }
  573.  
  574. # Option is the option letter
  575. # Value is the value being assigned
  576. # Name is the var name of the option, if any
  577. # ArgType is one of:
  578. # :    String argument
  579. # ;    Non-null string argument
  580. # *    Floating point argument
  581. # (    Non-negative floating point argument
  582. # )    Positive floating point argument
  583. # #    Integer argument
  584. # <    Non-negative integer argument
  585. # >    Positive integer argument
  586. # specGiven is the option specifier character use, if any (e.g. - or +),
  587. # for use in error messages.
  588. # Returns null on success, err string on error
  589. function CheckType(ArgType,Value,Option,Name,specGiven,  Err,ErrStr) {
  590.     if (ArgType == ":")
  591.     return ""
  592.     if (ArgType == ";") {
  593.     if (Value == "")
  594.         Err = "must be a non-empty string"
  595.     }
  596.     # A number begins with optional + or -, and is followed by a string of
  597.     # digits or a decimal with digits before it, after it, or both
  598.     else if (Value !~ /^[-+]?([0-9]+|[0-9]*\.[0-9]+|[0-9]+\.)$/)
  599.     Err = "must be a number"
  600.     else if (ArgType ~ "[#<>]" && Value ~ /\./)
  601.     Err = "may not include a fraction"
  602.     else if (ArgType ~ "[()<>]" && Value < 0)
  603.     Err = "may not be negative"
  604.     # (
  605.     else if (ArgType ~ "[)>]" && Value == 0)
  606.     Err = "must be a positive number"
  607.     if (Err != "") {
  608.     ErrStr = "Bad value \"" Value "\".  Value assigned to "
  609.     if (Name != "")
  610.         return ErrStr "variable " substr(Name,1,1) " " Err
  611.     else {
  612.         if (Option == "&")
  613.         Option = Value
  614.         return ErrStr "option " specGiven substr(Option,1,1) " " Err
  615.     }
  616.     }
  617.     else
  618.     return ""
  619. }
  620.  
  621. # Note: only the above functions are needed by ProcArgs.
  622. # The rest of these functions call ProcArgs() and also do other
  623. # option-processing stuff.
  624.  
  625. # Opts: Process command line arguments.
  626. # Opts processes command line arguments using ProcArgs()
  627. # and checks for errors.  If an error occurs, a message is printed
  628. # and the program is exited.
  629. #
  630. # Input variables:
  631. # Name is the name of the program, for error messages.
  632. # Usage is a usage message, for error messages.
  633. # OptList the option description string, as used by ProcArgs().
  634. # MinArgs is the minimum number of non-option arguments that this
  635. # program should have, non including ARGV[0] and +h.
  636. # If the program does not require any non-option arguments,
  637. # MinArgs should be omitted or given as 0.
  638. # rcFiles, if given, is a colon-seprated list of filenames to read for
  639. # variable initialization.  If a filename begins with ~/, the ~ is replaced
  640. # by the value of the environment variable HOME.  If a filename begins with
  641. # $, the part from the character after the $ up until (but not including)
  642. # the first character not in [a-zA-Z0-9_] will be searched for in the
  643. # environment; if found its value will be substituted, if not the filename will
  644. # be discarded.
  645. # rcfiles are read in the order given.
  646. # Values given in them will not override values given on the command line,
  647. # and values given in later files will not override those set in earlier
  648. # files, because AssignVal() will store each with a different instance index.
  649. # The first instance of each variable, either on the command line or in an
  650. # rcfile, will be stored with no instance index, and this is the value
  651. # normally used by programs that call this function.
  652. # VarNames is a comma-separated list of variable names to map to options,
  653. # in the same order as the options are given in OptList.
  654. # If EnvSearch is given and nonzero, the first EnvSearch variables will also be
  655. # searched for in the environment.  If set to -1, all values will be searched
  656. # for in the environment.  Values given in the environment will override
  657. # those given in the rcfiles but not those given on the command line.
  658. # NoRCopt, if given, is an additional letter option that if given on the
  659. # command line prevents the rcfiles from being read.
  660. # See ProcArgs() for a description of AllowUnRecOpt and optChars, and
  661. # ExclusiveOptions() for a description of exOpts.
  662. # Special options:
  663. # If x is made an option and is given, some debugging info is output.
  664. # h is assumed to be the help option.
  665.  
  666. # Global variables:
  667. # The command line arguments are taken from ARGV[].
  668. # The arguments that are option specifiers and values are removed from
  669. # ARGV[], leaving only ARGV[0] and the non-option arguments.
  670. # The number of elements in ARGV[] should be in ARGC.
  671. # After processing, ARGC is set to the number of elements left in ARGV[].
  672. # The option values are put in Options[].
  673. # On error, Err is set to a positive integer value so it can be checked for in
  674. # an END block.
  675. # Return value: The number of elements left in ARGV is returned.
  676. # Must keep OptErr global since it may be set by InitOpts().
  677. function Opts(Name,Usage,OptList,MinArgs,rcFiles,VarNames,EnvSearch,NoRCopt,
  678. AllowUnrecOpt,optChars,exOpts,  ArgsLeft,e) {
  679.     if (MinArgs == "")
  680.     MinArgs = 0
  681.     ArgsLeft = ProcArgs(ARGC,ARGV,OptList NoRCopt,Options,1,AllowUnrecOpt,
  682.     optChars)
  683.     if (ArgsLeft < (MinArgs+1) && !("h" in Options)) {
  684.     if (ArgsLeft >= 0) {
  685.         OptErr = "Not enough arguments"
  686.         Err = 4
  687.     }
  688.     else
  689.         Err = -ArgsLeft
  690.     printf "%s: %s.\nUse -h for help.\n%s\n",
  691.     Name,OptErr,Usage > "/dev/stderr"
  692.     exit 1
  693.     }
  694.     if (rcFiles != "" && (NoRCopt == "" || !(NoRCopt in Options)) &&
  695.     (e = InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch)) < 0)
  696.     {
  697.     print Name ": " OptErr ".\nUse -h for help." > "/dev/stderr"
  698.     Err = -e
  699.     exit 1
  700.     }
  701.     if ((exOpts != "") && ((OptErr = ExclusiveOptions(exOpts,Options)) != ""))
  702.     {
  703.     printf "%s: Error: %s\n",Name,OptErr > "/dev/stderr"
  704.     Err = 1
  705.     exit 1
  706.     }
  707.     return ArgsLeft
  708. }
  709.  
  710. # ReadConfFile(): Read a file containing var/value assignments, in the form
  711. # <variable-name><assignment-char><value>.
  712. # Whitespace (spaces and tabs) around a variable (leading whitespace on the
  713. # line and whitespace between the variable name and the assignment character) 
  714. # is stripped.  Lines that do not contain an assignment operator or which
  715. # contain a null variable name are ignored, other than possibly being noted in
  716. # the return value.  If more than one assignment is made to a variable, the
  717. # first assignment is used.
  718. # Input variables:
  719. # File is the file to read.
  720. # Comment is the line-comment character.  If it is found as the first non-
  721. #     whitespace character on a line, the line is ignored.
  722. # Assign is the assignment string.  The first instance of Assign on a line
  723. #     separates the variable name from its value.
  724. # If StripWhite is true, whitespace around the value (whitespace between the
  725. #     assignment char and trailing whitespace on the line) is stripped.
  726. # VarPat is a pattern that variable names must match.  
  727. #     Example: "^[a-zA-Z][a-zA-Z0-9]+$"
  728. # If FlagsOK is true, variables are allowed to be "set" by being put alone on
  729. #     a line; no assignment operator is needed.  These variables are set in
  730. #     the output array with a null value.  Lines containing nothing but
  731. #     whitespace are still ignored.
  732. # Output variables:
  733. # Values[] contains the assignments, with the indexes being the variable names
  734. #     and the values being the assigned values.
  735. # Lines[] contains the line number that each variable occured on.  A flag set
  736. #     is record by giving it an index in Lines[] but not in Values[].
  737. # Return value:
  738. # If any errors occur, a string consisting of descriptions of the errors
  739. # separated by newlines is returned.  In no case will the string start with a
  740. # numeric value.  If no errors occur,  the number of lines read is returned.
  741. function ReadConfigFile(Values,Lines,File,Comment,Assign,StripWhite,VarPat,
  742. FlagsOK,
  743. Line,Status,Errs,AssignLen,LineNum,Var,Val) {
  744.     if (Comment != "")
  745.     Comment = "^" Comment
  746.     AssignLen = length(Assign)
  747.     if (VarPat == "")
  748.     VarPat = "."    # null varname not allowed
  749.     while ((Status = (getline Line < File)) == 1) {
  750.     LineNum++
  751.     sub("^[ \t]+","",Line)
  752.     if (Line == "")        # blank line
  753.         continue
  754.     if (Comment != "" && Line ~ Comment)
  755.         continue
  756.     if (Pos = index(Line,Assign)) {
  757.         Var = substr(Line,1,Pos-1)
  758.         Val = substr(Line,Pos+AssignLen)
  759.         if (StripWhite) {
  760.         sub("^[ \t]+","",Val)
  761.         sub("[ \t]+$","",Val)
  762.         }
  763.     }
  764.     else {
  765.         Var = Line    # If no value, var is entire line
  766.         Val = ""
  767.     }
  768.     if (!FlagsOK && Val == "") {
  769.         Errs = Errs \
  770.         sprintf("\nBad assignment on line %d of file %s: %s",
  771.         LineNum,File,Line)
  772.         continue
  773.     }
  774.     sub("[ \t]+$","",Var)
  775.     if (Var !~ VarPat) {
  776.         Errs = Errs sprintf("\nBad variable name on line %d of file %s: %s",
  777.         LineNum,File,Var)
  778.         continue
  779.     }
  780.     if (!(Var in Lines)) {
  781.         Lines[Var] = LineNum
  782.         if (Pos)
  783.         Values[Var] = Val
  784.     }
  785.     }
  786.     if (Status)
  787.     Errs = Errs "\nCould not read file " File
  788.     close(File)
  789.     return Errs == "" ? LineNum : substr(Errs,2)    # Skip first newline
  790. }
  791.  
  792. # Variables:
  793. # Data is stored in Options[].
  794. # rcFiles, OptList, VarNames, and EnvSearch are as as described for Opts().
  795. # Global vars:
  796. # Sets OptErr.  Uses ENVIRON[].
  797. # If anything is read from any of the rcfiles, sets READ_RCFILE to 1.
  798. function InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch,
  799. Line,Var,Pos,Vars,Map,CharOpt,NumVars,TypesInd,Types,Type,Ret,i,rcFile,
  800. fNames,numrcFiles,filesRead,Err,Values,retStr) {
  801.     split("",filesRead,"")    # make awk know this is an array
  802.     NumVars = split(VarNames,Vars,",")
  803.     TypesInd = Ret = 0
  804.     if (EnvSearch == -1)
  805.     EnvSearch = NumVars
  806.     for (i = 1; i <= NumVars; i++) {
  807.     Var = Vars[i]
  808.     CharOpt = substr(OptList,++TypesInd,1)
  809.     if (CharOpt ~ "^[:;*()#<>&]$")
  810.         CharOpt = substr(OptList,++TypesInd,1)
  811.     Map[Var] = CharOpt
  812.     Types[Var] = Type = substr(OptList,TypesInd+1,1)
  813.     # Do not overwrite entries from environment
  814.     if (i <= EnvSearch && Var in ENVIRON &&
  815.     (Err = AssignVal(CharOpt,ENVIRON[Var],Options,Type,1,Var,0)) < 0)
  816.         return Err
  817.     }
  818.  
  819.     numrcFiles = split(rcFiles,fNames,":")
  820.     for (i = 1; i <= numrcFiles; i++) {
  821.     rcFile = fNames[i]
  822.     if (rcFile ~ "^~/")
  823.         rcFile = ENVIRON["HOME"] substr(rcFile,2)
  824.     else if (rcFile ~ /^\$/) {
  825.         rcFile = substr(rcFile,2)
  826.         match(rcFile,"^[a-zA-Z0-9_]*")
  827.         envvar = substr(rcFile,1,RLENGTH)
  828.         if (envvar in ENVIRON)
  829.         rcFile = ENVIRON[envvar] substr(rcFile,RLENGTH+1)
  830.         else
  831.         continue
  832.     }
  833.     if (rcFile in filesRead)
  834.         continue
  835.     # rcfiles are liable to be given more than once, e.g. UHOME and HOME
  836.     # may be the same
  837.     filesRead[rcFile]
  838.     if ("x" in Options)
  839.         printf "Reading configuration file %s\n",rcFile > "/dev/stderr"
  840.     retStr = ReadConfigFile(Values,Lines,rcFile,"#","=",0,"",1)
  841.     if (retStr > 0)
  842.         READ_RCFILE = 1
  843.     else if (ret != "") {
  844.         OptErr = retStr
  845.         Ret = -1
  846.     }
  847.     for (Var in Lines)
  848.         if (Var in Map) {
  849.         if ((Err = AssignVal(Map[Var],
  850.         Var in Values ? Values[Var] : "",Options,Types[Var],
  851.         Var in Values,Var,0)) < 0)
  852.             return Err
  853.         }
  854.         else {
  855.         OptErr = sprintf(\
  856.         "Unknown var \"%s\" assigned to on line %d\nof file %s",Var,
  857.         Lines[Var],rcFile)
  858.         Ret = -1
  859.         }
  860.     }
  861.  
  862.     if ("x" in Options)
  863.     for (Var in Map)
  864.         if (Map[Var] in Options)
  865.         printf "(%s) %s=%s\n",Map[Var],Var,Options[Map[Var]] > \
  866.         "/dev/stderr"
  867.         else
  868.         printf "(%s) %s not set\n",Map[Var],Var > "/dev/stderr"
  869.     return Ret
  870. }
  871.  
  872. # OptSets is a semicolon-separated list of sets of option sets.
  873. # Within a list of option sets, the option sets are separated by commas.  For
  874. # each set of sets, if any option in one of the sets is in Options[] AND any
  875. # option in one of the other sets is in Options[], an error string is returned.
  876. # If no conflicts are found, nothing is returned.
  877. # Example: if OptSets = "ab,def,g;i,j", an error will be returned due to
  878. # the exclusions presented by the first set of sets (ab,def,g) if:
  879. # (a or b is in Options[]) AND (d, e, or f is in Options[]) OR
  880. # (a or b is in Options[]) AND (g is in Options) OR
  881. # (d, e, or f is in Options[]) AND (g is in Options)
  882. # An error will be returned due to the exclusions presented by the second set
  883. # of sets (i,j) if: (i is in Options[]) AND (j is in Options[]).
  884. # todo: make options given on command line unset options given in config file
  885. # todo: that they conflict with.
  886. function ExclusiveOptions(OptSets,Options,
  887. Sets,SetSet,NumSets,Pos1,Pos2,Len,s1,s2,c1,c2,ErrStr,L1,L2,SetSets,NumSetSets,
  888. SetNum,OSetNum) {
  889.     NumSetSets = split(OptSets,SetSets,";")
  890.     # For each set of sets...
  891.     for (SetSet = 1; SetSet <= NumSetSets; SetSet++) {
  892.     # NumSets is the number of sets in this set of sets.
  893.     NumSets = split(SetSets[SetSet],Sets,",")
  894.     # For each set in a set of sets except the last...
  895.     for (SetNum = 1; SetNum < NumSets; SetNum++) {
  896.         s1 = Sets[SetNum]
  897.         L1 = length(s1)
  898.         for (Pos1 = 1; Pos1 <= L1; Pos1++)
  899.         # If any of the options in this set was given, check whether
  900.         # any of the options in the other sets was given.  Only check
  901.         # later sets since earlier sets will have already been checked
  902.         # against this set.
  903.         if ((c1 = substr(s1,Pos1,1)) in Options)
  904.             for (OSetNum = SetNum+1; OSetNum <= NumSets; OSetNum++) {
  905.             s2 = Sets[OSetNum]
  906.             L2 = length(s2)
  907.             for (Pos2 = 1; Pos2 <= L2; Pos2++)
  908.                 if ((c2 = substr(s2,Pos2,1)) in Options)
  909.                 ErrStr = ErrStr "\n"\
  910.                 sprintf("Cannot give both %s and %s options.",
  911.                 c1,c2)
  912.             }
  913.     }
  914.     }
  915.     if (ErrStr != "")
  916.     return substr(ErrStr,2)
  917.     return ""
  918. }
  919.  
  920. # The value of each instance of option Opt that occurs in Options[] is made an
  921. # index of Set[].
  922. # The return value is the number of instances of Opt in Options.
  923. function Opt2Set(Options,Opt,Set,  count) {
  924.     if (!(Opt in Options))
  925.     return 0
  926.     Set[Options[Opt]]
  927.     count = Options[Opt,"count"]
  928.     for (; count > 1; count--)
  929.     Set[Options[Opt,count]]
  930.     return count
  931. }
  932.  
  933. # The value of each instance of option Opt that occurs in Options[] that
  934. # begins with "!" is made an index of nSet[] (with the ! stripped from it).
  935. # Other values are made indexes of Set[].
  936. # The return value is the number of instances of Opt in Options.
  937. function Opt2Sets(Options,Opt,Set,nSet,  count,aSet,ret) {
  938.     ret = Opt2Set(Options,Opt,aSet)
  939.     for (value in aSet)
  940.     if (substr(value,1,1) == "!")
  941.         nSet[substr(value,2)]
  942.     else
  943.         Set[value]
  944.     return ret
  945. }
  946.  
  947. # Returns true if option Opt was given on the command line.
  948. function CmdLineOpt(Options,Opt,  i) {
  949.     for (i = 1; (Opt,"num",i) in Options; i++)
  950.     if (Options[Opt,"num",i] != 0)
  951.         return 1
  952.     return 0
  953. }
  954. ### End of ProcArgs library
  955.  
  956. # Return (in Ind) the indices of the elements with the smallest value in A.
  957. # The smallest value is returned as the function value.
  958. # If there are no elements in A, null is returned.
  959. function arrMin(A,Ind,  i,min) {
  960.     for (i in A)
  961.     if (min == "" || A[i] < min) {
  962.         DeleteAll(Ind)
  963.         min = A[i]
  964.         Ind[i]
  965.     }
  966.     else if (A[i] == min)
  967.         Ind[i]
  968.     return min
  969. }
  970.  
  971. # Remove all elements from Set
  972. function DeleteAll(Set,  i) {
  973.     for (i in Set)
  974.     delete Set[i]
  975. }
  976.